package com.hzb.myvpn.model;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;

import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.net.VpnService;
import android.preference.PreferenceManager;
import android.util.Log;

import com.hzb.myvpn.model.ITunDev.ITunDevFactory;

public class MyVpn extends BaseVpn {
    private VpnService service;
    private static Random random = new Random();
    private int seed;
    
    public int getSeed() {
        return seed;
    }
    
    public MyVpn(VpnConfig conf, ITunDevFactory tunFactory, VpnService service) throws IOException {
        super(conf, tunFactory);
        this.service = service;
    }

    byte[][] EORS = {"\n\r\n".getBytes(), "\n\n".getBytes()};
    
    private String readResponse(InputStream ins) throws IOException {
        byte[] buffer = new byte[100000];
        int offset = 0;
        for (;;) {
            int nread = ins.read(buffer, offset, buffer.length - offset);
            if (nread < 0) return null;
            offset += nread;
            for (byte[] EOR : EORS) {
                byte[] tail = Arrays.copyOfRange(buffer, offset - EOR.length, offset);
                if (Arrays.equals(tail, EOR)) {
                    return new String(buffer, 0, offset);
                }
            }
        }
    }

    private Map<String, String> parseHeaders(String resp) {
        String[] segs = resp.split("\n");
        Map<String, String> ret = new HashMap<String, String>();
        for (int i=1; i<segs.length; i++) {
            String[] tmp = segs[i].split(": ", 2);
            if (tmp.length == 2) {
                ret.put(tmp[0].trim(), tmp[1].trim());
            }
        }
        return ret;
    }
    @SuppressWarnings("unchecked")
    @Override
    protected ConnInfo connect() throws Exception {
        boolean success = false;
        InputStream ins = null;
        OutputStream outs = null;
        SocketChannel sc = SocketChannel.open();
        Socket socket = sc.socket();
        socket.setTcpNoDelay(true);

        try {
            socket.connect(new InetSocketAddress(config.getHost(), config.getPort()), 30000);
            socket.setSoTimeout(30000);
            ins = socket.getInputStream();
            outs = socket.getOutputStream();
            
            outs.write(String.format("GET /auth HTTP/1.1\r\n" +
                    "Host: lab.qq.com\r\n" +
                    "Connection: keep-alive\r\n" +
                    "Seed: %d\r\n" +
                    "User-Agent: android\r\n\r\n",
                    this.seed).getBytes());
            String resp = readResponse(ins);
            if (!resp.startsWith("HTTP/1.1 401 Unauthorized")) {
                Log.e("Vpn", "auth response status error");
                return null;
            }
            Map<String, String> headers = parseHeaders(resp);
            String seed = headers.get("Seed");
            if (seed == null) {
                Log.e("Vpn", "No Seed in auth error");
                return null;
            }
            this.seed = Integer.parseInt(seed);
            byte[] digest = MessageDigest.getInstance("MD5").digest((seed + config.getPassword()).getBytes());
            
            String digestStr = new BigInteger(1, digest).toString(16);
            if (digestStr.length() != 32) {
                StringBuilder str = new StringBuilder();
                for (int i=digestStr.length(); i<32; i++) {
                    str.append('0');
                }
                str.append(digestStr);
                digestStr = str.toString();
            }
    
            outs.write(String.format("GET /auth2 HTTP/1.1\r\n" +
                    "Host: lab.qq.com\r\n" +
                    "Connection: keep-alive\r\n" +
                    "User-Agent: android\r\n" +
                    "Username: %s\r\n" +
                    "Digest: %s\r\n\r\n",
                    config.getUsername(), digestStr
                    ).getBytes());
            resp = readResponse(ins);
            if (!resp.startsWith("HTTP/1.1 200 OK")) {
                Log.e("Vpn", "auth2 response status error");
                return null;
            }

            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(service);
            int pushRouteVer = prefs.getInt("PushRouteVer", 0);

            outs.write(
                    String.format("POST /download HTTP/1.1\r\n" +
                            "Host: lab.qq.com\r\n" +
                            "Connection: keep-alive\r\n" +
                            "User-Agent: android\r\n" +
                            "RouteConfVer: %d\r\n" +
                            "Heartbeat: %d %d\r\n" +
                            "Transfer-Encoding: chunked\r\n\r\n",
                            pushRouteVer,
                            config.getServerHeartbeat(), config.getServerTimeout()
                            ).getBytes());
            resp = readResponse(ins);
            if (!resp.startsWith("HTTP/1.1 200 OK")) {
                Log.e("Vpn", "download response status error");
                return null;
            }
            headers = parseHeaders(resp);
            List<Route> pushRoutes = null;
            File routesCache = service.getFileStreamPath("routes.cache");
            try {
                if (headers.containsKey("PushRouteVer")) {
                    pushRouteVer = Integer.valueOf(headers.get("PushRouteVer"));
                    Editor editor = prefs.edit();
                    editor.putInt("PushRouteVer", pushRouteVer);
                    editor.commit();
                    String[] pushRoutesStr = headers.get("PushRoutes").split(",");
                    pushRoutes = new LinkedList<Route>();
                    for (String routeStr : pushRoutesStr) {
                        String[] tmp = routeStr.split("/");
                        Route route = new Route(tmp[0], 32);
                        if (tmp.length == 2) route.setPrefix(Integer.parseInt(tmp[1]));
                        pushRoutes.add(route);
                    }
                    ObjectOutputStream fout = new ObjectOutputStream(new FileOutputStream(routesCache));
                    try {
                        fout.writeObject(pushRoutes);
                    } finally {
                        fout.close();
                    }
                } else if (routesCache.exists()) {
                    ObjectInputStream fin = new ObjectInputStream(new FileInputStream(routesCache));
                    try {
                        pushRoutes = (List<Route>) fin.readObject();
                    } finally {
                        fin.close();
                    }
                }
            } catch (Exception e) {
                Log.e("Vpn", "Error parsing/saving push routes", e);
            }

            ConnInfo ret = new ConnInfo();
            ret.setLocalIp(new String(headers.get("ClientIp")));
            ret.setRemoteIp(new String(headers.get("ServerIp")));
            ret.setRoutes(pushRoutes);
            ret.setSocket(sc);
            success = true;
            return ret;
        } finally {
            if (!success) {
                try {
                    if (ins != null) ins.close();
                } catch (Exception e) {
                    Log.e("Vpn", "Error closing InputStream", e);
                }
                try {
                    if (outs != null) outs.close();
                } catch (Exception e) {
                    Log.e("Vpn", "Error closing OutputStream", e);
                }
                try {
                    socket.close();
                } catch (Exception e) {
                    Log.e("Vpn", "Error closing Socket", e);
                }
                try {
                    sc.close();
                } catch (Exception e) {
                    Log.e("Vpn", "Error closing SocketChannel", e);
                }
            }
        }
        
    }
    private static int prime_num[] = {3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97};

    @Override
    protected void encode(byte[] src, short size, ByteBuffer dst)
            throws Exception {
        int seed = prime_num[random.nextInt(prime_num.length)];
        int seed2 = size - seed;
        
        for (int i=0; i<size; i++) {
            src[i] = (byte)(((src[i] & 0xff) + i * seed + seed2) % 256);
        }
        dst.putShort((short)seed);
        dst.putShort((short)(size - seed));
        dst.put(src, 0, size);
        int padding = (size + 4) % 16;
        if (padding != 0) {
            dst.position(dst.position() + 16 - padding);
        }
    }

    @Override
    protected short decode(ByteBuffer src, byte[] dst) throws Exception {
        if (src.remaining() < 16) return -1;
        short seed = src.getShort();
        short seed2 = src.getShort();
        short size = (short) (seed + seed2);
        int padding = (size + 4) % 16;
        padding = padding == 0 ? 0 : 16 - padding;
        if (src.remaining() >= size + padding) {
            src.get(dst, 0, size);
            if (padding > 0) src.position(src.position() + padding);
            for (int i=0; i<size; i++) {
                dst[i] = (byte)(((dst[i] & 0xff) - i * seed - seed2) % 256);
            }
            return size;
        } else {
            src.position(src.position() - 4);
            return -1;
        }
    }

}
